﻿# coding=utf-8
# 2023/4/27: 首个记录，基于旧2020/8/18版本，支持插件版本6.0，增加template_postfix全局变量，增加get/set_global_var
# 2023/5/4: 添加注释，支持自动生成文档
# 2023/5/5: 移除EnumValue
# 2023/6/16: 修正DummyAgency。移除Util.__init__
# 2025/5/15: 总线通道数扩展至48
# 2025/7/15: 新增python3_ver_minor

""" 此模块为运行Python模块所需的通用模块

应用程序调用模式下，此模块将被自动导入，在脚本中以bi.调用相关功能即可。
独立运行模式下，需要在入口脚本(如main.py)中显式导入bi_common(import bi_common as bi)和bi_standalone模块。
"""

plugin_ver_major = 6
""" int API主版本号，对应插件主版本 """

plugin_ver_minor = 5
""" int API次版本号，对应插件次版本 """

python3_ver_minor = 0
""" int Python3环境的次版本号，比如8表示3.8 """

standalone = True
""" bool 是否为独立运行模式，True表示为独立运行模式，False表示从应用程序调用 """

global_public_path = ""
""" str 全局公共数据路径 """

time = 0.0
""" float 运行脚本对应的时间偏置，与输入信号、同步样本的时间一致，单位秒 """

interval = 0.0
""" float 循环时间间隔，单位秒 """

session = None
""" datetime.datetime 当前Session ID """

session_public_path = ""
""" str 当前Session公共数据路径 """

input_samples = {}
""" dict[str, GeneralSamplePair] 输入样本表，键为样本协议ID，值为对应的通用样本对 """

output_samples = {}
""" dict[str, GeneralSample | list[GeneralSample]] 输出样本表，键为样本协议ID，值为通用样本或样本列表。建议使用bi.Util.output_sample输出样本 """

report_id_table = {}
""" dict[str, int] 报告ID映射表，键为报告的本地ID，值为对应的全局ID """

template_postfix = ""
""" str 模板后缀ID """


class GeneralSample:
    def __init__(self):
        """ 通用样本类 """

        self.protocol = ""
        """ str 样本协议ID """

        self.time = 0.0
        """ float 样本时间偏置，单位秒 """

        self.values = []
        """ list[float | str | None] 样本的各元素值 """

        self.significant = 0
        """ int 样本关键元素个数，采集时关键元素将保存至csv表格 """


class GeneralSamplePair:
    def __init__(self):
        """ 通用样本对类 """

        self.sample1 = GeneralSample()
        """ GeneralSample 前序样本 """

        self.weight1 = 0.0
        """ float 前序样本权重 """

        self.sample2 = GeneralSample()
        """ GeneralSample 后序样本 """

        self.weight2 = 0.0
        """ float 后序样本权重 """


class Scene:
    def __init__(self):
        """ 场景片段类 """

        self.id = ""
        """ str 场景ID，应含模板后缀ID（如有） """

        self.begin = 0.0
        """ float 场景片段开始时间，单位秒 """

        self.length = 0.0
        """ float 场景片段长度，单位秒 """

        self.properties = []
        """ list[str] 场景片段属性值 """


class BusMessage:
    def __init__(self):
        """ 总线报文类 """

        self.channel = 0
        """ int 总线通道，1~48，0表示无效 """

        self.id = 0
        """ int 报文ID """

        self.periodic = False
        """ bool 是否为周期发送 """

        self.period = 0
        """ int 报文发送周期（仅当periodic为True时有效），单位毫秒 """

        self.data = []
        """ list[int] 报文数据，各元素为字节值，范围为0~255 """


class DummyAgency:
    def __init__(self):
        """ 无效的功能接口实现 """
        pass

    def create_general_sample(self):
        """ 创建通用样本对象
        
        Returns:
            GeneralSample 创建的通用样本对象
        """
        return None

    def create_general_sample_pair(self):
        """ 创建通用样本对对象
        
        Returns:
            GeneralSamplePair 创建的通用样本对对象
        """
        return None

    def create_bus_message(self):
        """ 创建总线报文对象

        Returns:
            BusMessage 创建的总线报文对象
        """
        return None

    def create_scene(self):
        """ 创建场景片段对象
        
        Returns:
            Scene 创建的场景片段对象
        """
        return None

    def get_global_param(self, key):
        """ 获取应用程序全局参数
        
        Args:
            key: str 应用程序全局参数的关键字
        Returns:
            str 应用程序全局参数值，若不存在返回None
        """
        return None

    def set_global_param(self, key, value):
        """ 设置应用程序全局参数
        
        Args:
            key: str 应用程序全局参数的关键字
            value: str 应用程序全局参数值
        """
        pass

    def add_scene(self, sess, scene):
        """ 添加一个场景片段
        
        若与既存场景片段有重合的则无法添加。
        建议使用bi.Util.add_scene添加场景片段。

        Args:
            sess: datetime.datetime 该场景所属session ID
            scene: Scene 添加的场景片段
        """
        pass

    def update_report(self, sess, global_id, parameters):
        """ 更新报告数据
        
        建议使用bi.Util中的函数更新报告。

        Args:
            sess: datetime.datetime 该报告所属session ID
            global_id: int 报告全局ID
            parameters: dict[str, Any] 用于更新报告的相关参数
        """
        pass

    def send_bus_message(self, msg):
        """ 发送总线报文
        
        仅在在线模式下有效。
        建议使用bi.Util.send_bus_message发送报文。

        Args:
            msg: BusMessage 需要发送的报文
        """
        pass

    def get_global_var(self, key):
        """ 获取应用程序全局变量
        
        Args:
            key: str 应用程序全局变量的关键字
        Returns:
            str 应用程序全局变量值，若不存在返回None
        """
        return None

    def set_global_var(self, key, value):
        """ 设置应用程序全局变量
        
        Args:
            key: str 应用程序全局变量的关键字
            value: str 应用程序全局变量值
        """
        pass


agency = DummyAgency()
""" 功能接口对象，用于提供框架层面的接口（都为静态函数），独立运行模式和应用程序调用模式对应不同的实现 """


class Util:
    """ 实用工具类 
    
    实用工具类用于提供应用层面的接口（都为静态函数），可简便的实现样本、报告等的I/O。
    """

    @staticmethod
    def output_sample(sample):
        """ 输出通用样本 
        
        Args:
            sample: GeneralSample 一般需要通过to_general_sample函数转为通用样本后才可输出
        """
        if sample is None or not hasattr(sample, 'protocol') or sample.protocol is None or len(sample.protocol) == 0:
            return
        protocol = sample.protocol
        if protocol not in output_samples:
            output_samples[protocol] = []
        elif not isinstance(output_samples[protocol], list):
            output_samples[protocol] = [output_samples[protocol]]
        output_samples[protocol].append(sample)

    @staticmethod
    def add_scene(scene):
        """ 添加一个场景片段
        
        若与既存场景片段有重合的则无法添加。

        Args:
            scene: Scene 添加的场景片段，其中id若未以模板后缀结尾则将自动补上（如有）
        """
        if session is None or scene is None or not hasattr(scene, 'id') or scene.id is None or len(scene.id) == 0:
            return
        if len(template_postfix) > 0:
            real_postfix = '-' + template_postfix
            if not scene.id.endswith(real_postfix):
                scene.id = scene.id + real_postfix
        agency.add_scene(session, scene)

    @staticmethod
    def set_to_value_report(id, value):
        """ 设置单值报告的值
        
        Args:
            id: str 报告的本地ID
            value: float 设置值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x_set': float(value)})

    @staticmethod
    def add_to_value_report(id, value):
        """ 累加数值至单值报告
        
        Args:
            id: str 报告的本地ID
            value: float 累加值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x': float(value)})

    @staticmethod
    def add_to_numeric_histline_report(id, x, hist_value, line_value = None):
        """ 添加样本数据至数值模式下的柱状折线图报告
        
        Args:
            id: str 报告的本地ID
            x: float 样本X轴数值
            hist_value: float 样本面向柱状图的Y轴数值
            line_value: float | None 样本面向折线图的Y轴数值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x': float(x), 'y': float(hist_value), 'y2': float(line_value) if line_value is not None else None})

    @staticmethod
    def add_hits_to_numeric_histline_report(id, x, hit, times = 1):
        """ 添加命中样本或不命中样本至数值模式下的柱状折线图报告（命中率模式）
        
        Args:
            id: str 报告的本地ID
            x: float 样本X轴数值
            hit: bool 是否命中
            times: int 添加次数
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x': float(x), 'hit': bool(hit), 'times': int(times)})

    @staticmethod
    def add_to_label_histline_report(id, label, hist_value, line_value = None):
        """ 添加样本数据至文字标签模式下的柱状折线图报告
        
        Args:
            id: str 报告的本地ID
            label: int 样本对应的文字标签序号，从0开始
            hist_value: float 样本面向柱状图的Y轴数值
            line_value: float | None 样本面向折线图的Y轴数值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x_index': int(label), 'y': float(hist_value), 'y2': float(line_value) if line_value is not None else None})

    @staticmethod
    def add_hits_to_label_histline_report(id, label, hit, times = 1):
        """ 添加命中样本或不命中样本至文字标签模式下的柱状折线图报告（命中率模式）
        
        Args:
            id: str 报告的本地ID
            label: int 样本对应的文字标签序号，从0开始
            hit: bool 是否命中
            times: int 添加次数
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x_index': int(label), 'hit': bool(hit), 'times': int(times)})

    @staticmethod
    def add_to_scatter_report(id, x, y):
        """ 添加样本数据至散点图报告
        
        Args:
            id: str 报告的本地ID
            x: float 样本X轴数值
            y: float 样本Y轴数值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x': float(x), 'y': float(y)})

    @staticmethod
    def add_to_scatter_report_with_time(id, x, y, t):
        """ 添加带时间戳的样本数据至散点图报告
        
        Args:
            id: str 报告的本地ID
            x: float 样本X轴数值
            y: float 样本Y轴数值
            t: float 样本时间偏置，单位秒
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x': float(x), 'y': float(y), 't': float(t), 'session': session.strftime('%Y%m%d%H%M%S')})

    @staticmethod
    def add_to_matrix_table_report(id, x, y, value):
        """ 添加样本数据至矩阵热力图报告

        Args:
            id: str 报告的本地ID
            x: float 样本X轴数值
            y: float 样本Y轴数值
            value: float 样本Z轴数值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x': float(x), 'y': float(y), 'z': float(value)})

    @staticmethod
    def add_to_label_table_report(id, x_label, y_label, value):
        """ 添加样本数据至表格热力图报告
        
        Args:
            id: str 报告的本地ID
            x_label: int 样本X轴对应的文字标签序号，从0开始
            y_label: int 样本Y轴对应的文字标签序号，从0开始
            value: float 样本Z轴数值
        """
        if id in report_id_table:
            agency.update_report(session, report_id_table[id], {'x_index': int(x_label), 'y_index': int(y_label), 'z': float(value)})

    @staticmethod
    def send_bus_message(channel, id, data, period = None):
        """ 发送总线报文
        
        仅在在线模式下有效。

        Args:
            channel: int 总线通道，1~48
            id: int 报文ID
            data: list[int] 报文数据，各元素为字节值，范围为0~255
            period: int | None 报文发送周期，若None则为单次发送
        """
        msg = agency.create_bus_message()
        msg.channel = channel
        msg.id = id
        msg.periodic = period is not None
        msg.period = period if period is not None else 0
        msg.data = data
        agency.send_bus_message(msg)

